<?php
// +-------------------------------------------------------------------------+
// | PHP Version 5                                                           |
// +-------------------------------------------------------------------------+
// | ImaEngine - web development framework                                   |
// | Copyright (c) 2006-2007 Sardar Yumatov <ja.doma@gmail.com>              |
// +-------------------------------------------------------------------------+
// | This library is free software; you can redistribute it and/or modify it |
// | under the terms of the GNU Lesser General Public License as published   |
// | by the Free Software Foundation; either version 2.1 of the License, or  |
// | (at your option) any later version.                                     |
// |                                                                         |
// | This library is distributed in the hope that it will be useful, but     |
// | WITHOUT ANY WARRANTY; without even the implied warranty of              |
// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                    |
// | See the GNU Lesser General Public License for more details.             |
// |                                                                         |
// | You should have received a copy of the GNU Lesser General Public        |
// | License along with this library; if not, write to the Free Software     |
// | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
// +-------------------------------------------------------------------------+

/**
* This simple boot script will limit bot/crawlers from overflooding the server by making
* many concurrent requests. Script will allow not more than N connections in M seconds.
* The bots's are recognized by UserAgent field. Normal users are unaffected.
*
* Warning: this script tries to recognize bots. Not all bots are known to this script.
* If you need to limit connections by IP use another script.
*
* There are two algoritms implemented, one is base on SQLite and anther on normal files.
* The SQLite database is fast, but currently doesn't have per row locking, only whole file locking
* This means all requests will be processed sequentially. If each bot is registered in
* its own database, then two different bot's will not block each other, but requests
* from a single bot will be still processed sequentially. Usually this is not a problem,
* because the processing time is matter of microseconds.
*
* Another algorithm creates files for each request. This requires much create/delete-file operations,
* but doesn't suffer from concurrent locking. If files gets created in shm or tempfs memory based
* filesystems, then performance will be probably much better than other approaches.
*
* @todo move SQLite functions to PDO because v3 support (faster, beter)
*
* @author Sardar Yumatov <ja.doma@gmail.com>
* @copyright Copyright &copy; 2006-2007 Sardar Yumatov
* @package boot
*
* @PHP5Only
* @config define BOT_CONCURRENT_BASE
*/

if(!defined('PROCESS')) {
	header('HTTP/1.1 404 Not Found');
	die('NotFound');
}

//Do not use Configuration, this script must be fast and simple as possible.

/**
 * Base directory for files ans sqlite databases.
 * This path must be absolute if you want to include this boot script from different entry points.
 * It is good idea to point this path to a tmpfs directory for overall good performance, espescially on 'files' driver.
 * @var string
 */
define('BOT_CONCURRENT_BASE', './data/botthrottle');

/**
 * The time limit for each connection, also the time frame within max connections are allowed.
 * @var integer
 */
define('BOT_CONCURRENT_TIME_LIMIT', 5);

/**
 * Connections limit for each bot.
 * @var integer
 */
define('BOT_CONCURRENT_CONNECTION_LIMIT', 10);

/**
 * Driver to use:
 *  - files - filebased, must be on tmpfs or alike for good performance
 *  - sqlite_single - sqlite based, single database for all bots, currently suffers from concurrent connections
 *  - sqlite_multi - sqlite based, apart database for each bot
 */
define('BOT_CONCURRENT_DRIVER', 'sqlite_multi');



//All requests without UserAgent are from bots per definition. Other popular bots are recognized too by UserAgent.
$isbotreg='/(googlebot|aport|yandex|stackrambler|slurp@inktomi|ask jeeves|lycos|whatuseek|ia_archiver)/i';
$botme = isset($_SERVER['HTTP_USER_AGENT'])? trim(strval($_SERVER['HTTP_USER_AGENT'])): null;

if(empty($botme) || preg_match($isbotreg, $botme)) { //здраствуй бот!
	$botme = empty($botme)? 'sardar_': strtolower($botme);
	if(!is_dir(BOT_CONCURRENT_BASE)) trigger_error('Boot statistics directory: "'.BOT_CONCURRENT_BASE.'" doesn\'t exists');
	else {
		if(BOT_CONCURRENT_DRIVER == 'files') { //file based driver
			if(!is_dir($path = BOT_CONCURRENT_BASE.DIRECTORY_SEPARATOR.$botme) && !mkdir($path)) trigger_error("Can't create bot directory '{$path}'! Probably no permissions!", E_USER_WARNING);
			else {
				$cur = 0; $now = time() - BOT_CONCURRENT_TIME_LIMIT; $rem=array();
				foreach(scandir($path) as $entry) {
					if($entry == '.' || $entry == '..') continue;
					if(filemtime($path.DIRECTORY_SEPARATOR.$entry) > $now) $cur++;
					else $rem[] = $entry;
				}
				if($cur > BOT_CONCURRENT_CONNECTION_LIMIT) {
					if(!headers_sent()) header('HTTP/1.0 202 Accepted, but throttled because of connection limit');
					die('Accepted, but throttled because of connection limit');
				} else {
					//fucking tempname() will create file somwhere else
					do {
						$file = $path.DIRECTORY_SEPARATOR.'botaccess_'.mt_rand();
					} while(file_exists($file));
					@touch($file);
				}

				//do we need cleanup
				if(sizeof($rem) > BOT_CONCURRENT_TIME_LIMIT * 3 || (time() % BOT_CONCURRENT_TIME_LIMIT) == 0) {
					foreach($rem as $entry) @unlink($path.DIRECTORY_SEPARATOR.$entry);
				}
				unset($cur, $path, $now, $rem, $entry, $file);
			}
		} elseif(BOT_CONCURRENT_DRIVER == 'sqlite_multi' || BOT_CONCURRENT_DRIVER == 'sqlite_single') { //SQLite base driver
			$db = BOT_CONCURRENT_BASE.DIRECTORY_SEPARATOR.(BOT_CONCURRENT_DRIVER == 'sqlite_multi'? $botme: '').'botstatistics.sqlite';
			$install = !is_file($db); //need to create new tables
			$mul = (BOT_CONCURRENT_DRIVER == 'sqlite_multi'); //multiple DB

			if(!is_null($db = sqlite_factory($db))) { //will create file if needed
				//IF NOT EXISTS is not supported by SQLite 2.1
				if($install && !$db->queryExec('CREATE TABLE botstat ('.(!$mul? 'botname VARCHAR(250) NOT NULL, ':'').'actime INTEGER NOT NULL);'.(!$mul? 'CREATE INDEX botname ON botstat (botname, actime);': 'CREATE INDEX actime ON botstat (actime);'))) die("Can't create botstat table!");
				$row = $db->query("SELECT COUNT(*) FROM botstat WHERE ".(!$mul? "botname='".sqlite_escape_string($botme)."' AND ": '')."actime > ".(time() - BOT_CONCURRENT_TIME_LIMIT).';');

				if(($row = $row->fetch(SQLITE_NUM)) !== false) {
					if($row[0] > BOT_CONCURRENT_CONNECTION_LIMIT) { //kill the request
						if(!headers_sent()) header('HTTP/1.0 202 Accepted, but throttled because of connection limit');
						die('Accepted, but throttled because of connection limit');
					} else {
						$db->queryExec("INSERT INTO botstat VALUES(".(!$mul? "'".sqlite_escape_string($botme)."', ": '').time().');');
						//clean up the garbage
						if((time() % BOT_CONCURRENT_TIME_LIMIT) == 0) $db->queryExec("DELETE FROM botstat WHERE ".(!$mul? "botname='".sqlite_escape_string($botme)."' AND ": '')."actime < ".(time()-BOT_CONCURRENT_TIME_LIMIT).';');
					}
				} else trigger_error('Can\'t retrive count of rows from SQLite DB', E_USER_WARNING);
				unset($install, $mul, $row, $db);
			} else trigger_error("Bot throttle: database 'botstatistics' is not accessible, review your installation!", E_USER_WARNING);
		} else trigger_error('Unknown request registering driver: '.BOT_CONCURRENT_DRIVER.'! Disabling botthrttle.php!', E_USER_WARNING);
	}
}

unset($isbotreg, $botme);
//GET /botthrottle.php HTTP/1.0
//Host: eurogates.ru
//UserAgent: test;
?>